import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
  AUTHZ_ENFORCER,
  AUTHZ_MODULE_OPTIONS,
  AuthZModuleOptions,
  PERMISSIONS_METADATA,
} from 'nest-authz';
import * as casbin from 'casbin';

@Injectable()
export class AuthZGuard implements CanActivate {
  private readonly logger = new Logger(AuthZGuard.name);

  constructor(
    private readonly reflector: Reflector,
    @Inject(AUTHZ_ENFORCER) private enforcer: casbin.Enforcer,
    @Inject(AUTHZ_MODULE_OPTIONS) private options: AuthZModuleOptions,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    try {
      const permissions: string[] = this.reflector.get<string[]>(
        PERMISSIONS_METADATA,
        context.getHandler(),
      );
      this.logger.debug('AuthZGuard.permissions:' + permissions);

      // empty or undefined callback
      if (!permissions) {
        return true;
      }

      // 读取登录用户
      const user = this.options.userFromContext(context);
      this.logger.debug('AuthZGuard.user:' + JSON.stringify(user));

      if (!user) {
        throw new UnauthorizedException();
      }

      // enforcer 角色权限检查
      return await AuthZGuard.asyncEvery<string>(
        permissions,
        async (permission) => this.enforcer.enforce(user, permission),
      );
    } catch (err) {
      this.logger.error(err);
      throw err;
    }
  }

  static async asyncSome<T>(
    array: T[],
    callback: (value: T, index: number, a: T[]) => Promise<boolean>,
  ): Promise<boolean> {
    for (let i = 0; i < array.length; i++) {
      const result = await callback(array[i], i, array);
      if (result) {
        return result;
      }
    }

    return false;
  }

  static async asyncEvery<T>(
    array: T[],
    callback: (value: T, index: number, a: T[]) => Promise<boolean>,
  ): Promise<boolean> {
    for (let i = 0; i < array.length; i++) {
      const result = await callback(array[i], i, array);
      if (!result) {
        return result;
      }
    }

    return true;
  }
}
